import argparse
from datetime import datetime, timezone
import json
import os
import re
import shutil
import subprocess
import sys
import time

import aiohttp
import asyncio
import git
import jinja2

from . import licensing, __version__

DEFAULT_EDITOR = "vi"
XDG_CACHE_HOME = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
CACHEDIR = os.path.join(XDG_CACHE_HOME, "go2rpm")
GIT_CACHEDIR = os.path.join(XDG_CACHE_HOME, "go2rpm", "src")
JINJA_ENV = jinja2.Environment(loader=jinja2.ChoiceLoader([
    jinja2.FileSystemLoader(["/"]),
    jinja2.PackageLoader("go2rpm", "templates"),
]),
                               extensions=["jinja2.ext.do"],
                               trim_blocks=True,
                               lstrip_blocks=True)


def detect_packager():
    rpmdev_packager = shutil.which("rpmdev-packager")
    if rpmdev_packager is not None:
        return subprocess.check_output(rpmdev_packager,
                                       universal_newlines=True).strip()

    gitbinary = shutil.which("git")
    if gitbinary is not None:
        name = subprocess.check_output([gitbinary, "config", "user.name"],
                                       universal_newlines=True).strip()
        email = subprocess.check_output([gitbinary, "config", "user.email"],
                                        universal_newlines=True).strip()
        return f"{name} <{email}>"
    return None


def file_mtime(path):
    return datetime.fromtimestamp(os.stat(path).st_mtime,
                                  timezone.utc).isoformat()


@jinja2.environmentfilter
def do_customwordwrap(environment, s, width=79, break_long_words=True,
                wrapstring=None, break_on_hyphens=False):
    """
    Return a copy of the string passed to the filter wrapped after
    ``79`` characters.  You can override this default using the first
    parameter.  If you set the second parameter to `false` Jinja will not
    split words apart if they are longer than `width`. By default, the newlines
    will be the default newlines for the environment, but this can be changed
    using the wrapstring keyword argument.
    """
    if not wrapstring:
        wrapstring = environment.newline_sequence
    import textwrap
    return wrapstring.join(textwrap.wrap(s, width=width, expand_tabs=False,
                                   replace_whitespace=False,
                                   break_long_words=break_long_words,
                                   break_on_hyphens=break_on_hyphens))


# Sanitize a Go import path that can then serve as rpm package name
# Mandatory parameter: a Go import path
def rpmname(goipath):
    # lowercase and end with '/'
    goname = goipath.lower() + '/'
    # remove eventual protocol prefix
    goname = re.sub(r"^http(s?)://", r"", goname)
    # remove eventual .git suffix
    goname = re.sub(r"\.git/*", r"", goname)
    # remove eventual git. prefix
    goname = re.sub(r"^git\.", r"", goname)
    # remove FQDN root (.com, .org, etc)
    # will also remove vanity FQDNs such as "tools"
    goname = re.sub(r"^([^/]+)\.([^\./]+)/", r"\g<1>/", goname)
    # add golang prefix
    goname = "golang-" + goname
    # special-case x.y.z number-strings as that’s an exception in our naming
    # guidelines
    while re.search(r"(\d)\.(\d)", goname):
        goname = re.sub(r"(\d)\.(\d)", r"\g<1>:\g<2>", goname)
    # replace various separators rpm does not like with -
    goname = re.sub(r"[\._/\-]+", r"-", goname)
    # because of the Azure sdk
    goname = re.sub(r"\-for\-go\-", r"-", goname)
    # Tokenize along - separators and remove duplicates to avoid
    # golang-foo-foo-bar-foo names
    result = ""
    tokens = {}
    tokens["go"] = True
    for token in goname.split('-'):
        if token not in tokens:
            result = result + "-" + token
            tokens[token] = True
    # reassemble the string, restore x.y.z runs, convert the vx.y.z
    # Go convention to x.y.z as prefered in rpm naming
    result = re.sub(r"^-", r"", result)
    result = re.sub(r"-$", r"", result)
    result = re.sub(r":", r".", result)
    # some projects have a name that end up in a number, and *also* add release
    # numbers on top of it, keep a - prefix before version strings
    result = re.sub(r"\-v([\.\d])", r"-\g<1>", result)
    return result


def has_cmd(goipath):
    path = os.path.join(GIT_CACHEDIR, *get_repo_name(goipath))
    cmd = os.path.isdir(os.path.join(path, 'cmd'))
    return cmd


def has_other_cmd(goipath):
    other_cmd = set()
    path = os.path.join(GIT_CACHEDIR, *get_repo_name(goipath))
    exclude = set([
        'cmd', 'vendor', 'example', 'examples', '_example', '_examples',
        'internal', 'Godeps', 'testdata', '_testdata', 'tests', 'test'
    ])
    for root, dirs, files in os.walk(path, topdown=True):
        dirs[:] = [d for d in dirs if d not in exclude]
        for file in files:
            if file.endswith(".go"):
                with open(os.path.join(root, file), "r") as f:
                    for line in f:
                        if line.startswith('package main'):
                            other_cmd.add(os.path.relpath(root, path))
                            break
    return list(other_cmd)


def detect_license(goipath):
    path = os.path.join(GIT_CACHEDIR, *get_repo_name(goipath))
    licenses = set()
    raw_licenses = subprocess.check_output(
        ['askalono', '--format', 'json', 'crawl', path],
        universal_newlines=True)
    raw_licenses = to_list(raw_licenses)
    for j in raw_licenses:
        try:
            if not 'vendor' in json.loads(j)['path']:
                licenses.add(json.loads(j)['result']['license']['name'])
        except KeyError:
            pass
    return ' and '.join(list(licenses))


def get_license_files(goipath):
    license_files = []
    path = os.path.join(GIT_CACHEDIR, *get_repo_name(goipath))
    exclude = set([
        'vendor', 'example', 'examples', '_example', '_examples', 'internal',
        'Godeps', 'testdata', '_testdata', '.github', 'tests', 'test'
    ])
    matcher = re.compile(
        r"(COPYING|COPYING[\.\-].*|COPYRIGHT|COPYRIGHT[\.\-].*|"
        r"EULA|EULA[\.\-].*|licen[cs]e|licen[cs]e.*|LICEN[CS]E|"
        r"LICEN[CS]E[\.\-].*|.*[\.\-]LICEN[CS]E.*|NOTICE|NOTICE[\.\-].*|"
        r"PATENTS|PATENTS[\.\-].*|UNLICEN[CS]E|UNLICEN[CS]E[\.\-].*|"
        r"agpl[\.\-].*|gpl[\.\-].*|lgpl[\.\-].*|AGPL-.*[0-9].*|"
        r"APACHE-.*[0-9].*|BSD-.*[0-9].*|CC-BY-.*|GFDL-.*[0-9].*|"
        r"GNU-.*[0-9].*|GPL-.*[0-9].*|LGPL-.*[0-9].*|MIT-.*[0-9].*|"
        r"MPL-.*[0-9].*|OFL-.*[0-9].*)")
    for root, dirs, files in os.walk(path, topdown=True):
        dirs[:] = [d for d in dirs if d not in exclude]
        for f in files:
            if matcher.match(f):
                license_files.append(
                    os.path.relpath(os.path.join(root, f), path))
    return license_files


def get_doc_files(goipath):
    doc_files = []
    path = os.path.join(GIT_CACHEDIR, *get_repo_name(goipath))
    include = set(
        ['doc', 'docs', 'example', 'examples', '_example', '_examples'])
    exclude = set([
        'vendor', 'doc', 'docs', 'example', 'examples', '_example',
        '_examples', 'internal', 'Godeps', 'testdata', '_testdata', '.github',
        'tests', 'test', '.circleci'
    ])
    matcher = re.compile(
        r"(.*\.md|.*\.markdown|.*\.mdown|.*\.mkdn|.*\.rst|.*\.txt|AUTHORS|"
        r"AUTHORS[\.\-].*|CONTRIBUTORS|CONTRIBUTORS[\.\-].*|README|"
        r"README[\.\-].*|CHANGELOG|CHANGELOG[\.\-].*|TODO|TODO[\.\-].*)",
        re.IGNORECASE)
    licensesex = re.compile(
        r"(COPYING|COPYING[\.\-].*|COPYRIGHT|COPYRIGHT[\.\-].*|EULA|"
        r"EULA[\.\-].*|licen[cs]e|licen[cs]e.*|LICEN[CS]E|LICEN[CS]E[\.\-].*|"
        r".*[\.\-]LICEN[CS]E.*|NOTICE|NOTICE[\.\-].*|PATENTS|PATENTS[\.\-].*|"
        r"UNLICEN[CS]E|UNLICEN[CS]E[\.\-].*|agpl[\.\-].*|gpl[\.\-].*|"
        r"lgpl[\.\-].*|AGPL-.*[0-9].*|APACHE-.*[0-9].*|BSD-.*[0-9].*|CC-BY-.*|"
        r"GFDL-.*[0-9].*|GNU-.*[0-9].*|GPL-.*[0-9].*|LGPL-.*[0-9].*|"
        r"MIT-.*[0-9].*|MPL-.*[0-9].*|OFL-.*[0-9].*|CMakeLists\.txt)")
    for root, dirs, files in os.walk(path, topdown=True):
        doc_files = doc_files + [d for d in dirs if d in include]
        dirs[:] = [d for d in dirs if d not in exclude]
        for f in files:
            if matcher.match(f) and not licensesex.match(f):
                doc_files.append(os.path.relpath(os.path.join(root, f), path))
    return doc_files


async def get_description(forge):
    owner = forge.split('/')[-2]
    repo = forge.split('/')[-1]
    if 'github.com' in forge:
        async with aiohttp.ClientSession() as session:
            url = f'https://api.github.com/repos/{owner}/{repo}'
            async with session.get(url) as resp:
                jsonresp = await resp.json()
                if 'message' in jsonresp:
                    return None
                else:
                    return normalize_description(jsonresp['description'])
    elif 'gitlab.com' in forge:
        async with aiohttp.ClientSession() as session:
            url = f'https://gitlab.com/api/v4/projects/{owner}%2F{repo}'
            async with session.get(url) as resp:
                jsonresp = await resp.json()
                if 'message' in jsonresp:
                    return None
                else:
                    return normalize_description(jsonresp['description'])
    elif 'bitbucket.org' in forge:
        async with aiohttp.ClientSession() as session:
            url = f'https://api.bitbucket.org/2.0/repositories/{owner}/{repo}'
            async with session.get(url) as resp:
                jsonresp = await resp.json()
                if 'error' in jsonresp:
                    return None
                else:
                    return normalize_description(jsonresp['description'])
    elif 'pagure.io' in forge:
        repo = '/'.join(forge.split('/')[3:])
        async with aiohttp.ClientSession() as session:
            url = f'https://pagure.io/api/0/{repo}'
            async with session.get(url) as resp:
                jsonresp = await resp.json()
                if 'error' in jsonresp:
                    return None
                else:
                    return normalize_description(jsonresp['description'])
    else:
        return None


def normalize_description(description):
    if description is not None:
        description = description.strip()
    else:
        return description
    if description != '':
        description = description[:1].upper() + description[1:]
    else:
        return None
    if not re.search(r"(\.|!)$", description):
        description = description + "."
    return description


def get_repo_name(goipath):
    url = goipath.split('/')
    return url


def get_repo_host(goipath):
    url = goipath.split('/')
    return url[0:3]


def download(goipath):
    os.environ['GOPATH'] = CACHEDIR
    os.environ['GO111MODULE'] = "off"
    try:
        subprocess.check_output(['go', 'get', '-d', goipath],
                                stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as err:
        if b'no Go files in' in err.stdout:
            try:
                subprocess.check_output(['go', 'get', '-d', goipath + '/...'],
                                        stderr=subprocess.STDOUT)
            except subprocess.CalledProcessError as err:
                print(f"ERROR: Unable to 'go get {goipath}':")
                print(err.stdout.decode())
                sys.exit(1)
        else:
            print(f"ERROR: Unable to 'go get {goipath}':")
            print(err.stdout.decode())
            sys.exit(1)
    repo = git.Repo(os.path.join(GIT_CACHEDIR, *get_repo_host(goipath)))
    repo.git.checkout("master")
    repo.head.reference = repo.heads.master
    repo.head.reset(index=True, working_tree=True)
    repo.remotes.origin.pull()


def get_version(goipath):
    repo = git.Repo(os.path.join(GIT_CACHEDIR, *get_repo_host(goipath)))
    tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime)
    if not len(tags):
        commit = str(repo.heads.master.commit)
        version = None
        tag = None
    else:
        latest = str(tags[-1])
        if latest.startswith('v'):
            version = latest[1:]
            tag = None
        else:
            version = None
            tag = latest
        tag_date = datetime.now(
            timezone.utc) - tags[-1].commit.committed_datetime
        if (tag_date.days > 365
                and repo.heads.master.commit.count() - tags[-1].commit.count()
                > 14):
            commit = str(repo.heads.master.commit)
        else:
            commit = None
    return version, tag, commit


def check_if_version_exists(goipath, version, tag, commit):
    path = os.path.join(GIT_CACHEDIR, *get_repo_host(goipath))
    repo = git.Repo(path)
    repo.git.checkout("master")
    repo.head.reference = repo.heads.master
    repo.head.reset(index=True, working_tree=True)
    repo.remotes.origin.pull()
    if commit:
        try:
            repo.git.checkout(commit)
        except git.GitCommandError:
            return False
    elif version:
        try:
            repo.git.checkout('v' + version)
        except git.GitCommandError:
            return False
    elif tag:
        try:
            repo.git.checkout(tag)
        except git.GitCommandError:
            return False
    return True


def set_repo_version(goipath, version, tag, commit):
    path = os.path.join(GIT_CACHEDIR, *get_repo_host(goipath))
    repo = git.Repo(path)
    repo.head.reference = repo.heads.master
    repo.head.reset(index=True, working_tree=True)
    repo.remotes.origin.pull()
    if commit:
        repo.git.checkout(commit)
    elif version:
        repo.git.checkout('v' + version)
    elif tag:
        repo.git.checkout(tag)


def get_buildrequires(goipath):
    os.environ['GOPATH'] = CACHEDIR
    os.environ['GO111MODULE'] = "off"
    buildrequires = subprocess.check_output([
        'golist', '--imported', '--skip-self', '--package-path', '/'.join(
            get_repo_host(goipath))
    ],
                                            universal_newlines=True)
    return buildrequires


def get_test_buildrequires(goipath):
    os.environ['GOPATH'] = CACHEDIR
    os.environ['GO111MODULE'] = "off"
    test_buildrequires = subprocess.check_output([
        'golist', '--imported', '--tests', '--skip-self', '--package-path',
        '/'.join(get_repo_host(goipath))
    ],
                                                 universal_newlines=True)
    return test_buildrequires


def to_list(s):
    if not s:
        return []
    return list(filter(None, (l.strip() for l in s.splitlines())))


def main():
    parser = argparse.ArgumentParser(
        "go2rpm", formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument("--show-license-map",
                        action="store_true",
                        help="Print license mappings and exit")
    parser.add_argument("--no-auto-changelog-entry",
                        action="store_true",
                        help="Do not generate a changelog entry")
    parser.add_argument("-",
                        "--stdout",
                        action="store_true",
                        help="Print spec into stdout")
    parser.add_argument("-p",
                        "--profile",
                        action="store",
                        nargs="?",
                        type=int,
                        choices=[1, 2],
                        default=2,
                        help="Profile of macros to use. \
                        1: old macros. 2: new macros. \
                        default: 2")
    parser.add_argument("-f",
                        "--forge",
                        action="store",
                        nargs="?",
                        help="Forge URL")
    parser.add_argument("-a",
                        "--altipaths",
                        action="store",
                        nargs="+",
                        help="List of alternate import paths")
    parser.add_argument("-v",
                        "--version",
                        action="store",
                        nargs="?",
                        help="Package version")
    parser.add_argument("-t",
                        "--tag",
                        action="store",
                        nargs="?",
                        help="Package tag")
    parser.add_argument("-c",
                        "--commit",
                        action="store",
                        nargs="?",
                        help="Package commit")
    parser.add_argument("-C",
                        "--clean",
                        action="store_true",
                        help="Clean cache for chosen Go import path")
    parser.add_argument("--clean-all",
                        action="store_true",
                        help="Clean all cached Go imports")
    parser.add_argument("goipath", help="Import path")
    args = parser.parse_args()

    if args.show_license_map:
        licensing.dump_sdpx_to_fedora_map(sys.stdout)
        return

    goipath = re.sub(r"^http(s?)://", r"", args.goipath)
    goipath = goipath.strip('/')

    known_forge = ('github.com', 'gitlab.com', 'bitbucket.org', 'pagure.io')
    known_forge_re = (r'^(' +
                      r'|'.join(re.escape(url) for url in known_forge) +
                      r')')
    if not re.search(known_forge_re, goipath) and args.forge is None:
        parser.error(
            f"forge URL is required for import path other than {known_forge}"
        )

    if args.forge is None:
        forge = 'https://' + goipath
    else:
        if not args.forge.startswith('http'):
            args.forge = 'https://' + args.forge
        forge = args.forge.strip('/')

    # Clean any existing repos, if requested.
    if args.clean_all:
        shutil.rmtree(GIT_CACHEDIR, ignore_errors=True)
    elif args.clean:
        shutil.rmtree(os.path.join(GIT_CACHEDIR, *get_repo_host(goipath)),
                      ignore_errors=True)

    # Download the repo
    download(goipath)

    # Sort out the versions
    if args.version is not None or args.tag is not None or args.commit is not None:
        if not check_if_version_exists(goipath, args.version, args.tag,
                                       args.commit):
            version, tag, commit = get_version(goipath)
        else:
            version, tag, commit = args.version, args.tag, args.commit
    else:
        version, tag, commit = get_version(goipath)

    # Prepare the repo
    set_repo_version(goipath, version, tag, commit)

    buildrequires = to_list(get_buildrequires(goipath))
    test_buildrequires = list(
        set(to_list(get_test_buildrequires(goipath))).difference(
            set(buildrequires)))

    description = asyncio.run(get_description(forge))
    if description is not None:
        summary = description[:-1]
    else:
        summary = None

    license_files = get_license_files(goipath)
    doc_files = get_doc_files(goipath)

    name = rpmname(goipath)
    cmd = has_cmd(goipath)
    other_cmd = has_other_cmd(goipath)
    if '.' in other_cmd:
        main_cmd = get_repo_name(goipath)[-1]
        other_cmd.remove('.')
    else:
        main_cmd = None

    JINJA_ENV.globals["to_list"] = to_list
    JINJA_ENV.filters["customwordwrap"] = do_customwordwrap
    if args.profile == 1:
        template = JINJA_ENV.get_template("profile1.spec")
    elif args.profile == 2:
        template = JINJA_ENV.get_template("profile2.spec")

    kwargs = {}	
    kwargs["generator_version"] = __version__
    kwargs["goipath"] = goipath
    kwargs["forge"] = forge
    kwargs["altipaths"] = args.altipaths

    kwargs["version"] = version
    kwargs["tag"] = tag
    kwargs["commit"] = commit

    kwargs["description"] = description
    kwargs["summary"] = summary

    kwargs["license_files"] = license_files
    kwargs["doc_files"] = doc_files

    kwargs["buildrequires"] = buildrequires
    kwargs["test_buildrequires"] = test_buildrequires

    kwargs["has_cmd"] = cmd
    kwargs["main_cmd"] = main_cmd
    kwargs["other_cmd"] = other_cmd

    if args.no_auto_changelog_entry:
        kwargs["auto_changelog_entry"] = False
    else:
        kwargs["auto_changelog_entry"] = True

    if version is None and tag is None:
        kwargs["pkg_release"] = "0.1"
    else:
        kwargs["pkg_release"] = "1"

    kwargs["date"] = time.strftime("%a %b %d %T %Z %Y")
    kwargs["shortdate"] = time.strftime("%Y%m%d")
    if commit is not None:
        kwargs["shortcommit"] = commit[:7]
    kwargs["packager"] = detect_packager()

    licenses = detect_license(goipath)
    if licenses != '':
        license, comments = licensing.translate_license(licenses)
        kwargs["licenses"] = licenses
        kwargs["license"] = license
        kwargs["license_comments"] = comments

    spec_file = f"{name}.spec"
    spec_contents = template.render(**kwargs)
    if args.stdout:
        print(f"# {spec_file}")
        print(spec_contents)
    else:
        with open(spec_file, "w") as fobj:
            fobj.write(spec_contents)
            fobj.write("\n")


if __name__ == "__main__":
    main()
